package com.ljt.springdataredisdemo;

import cn.hutool.json.JSONUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.io.*;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class HotSpotInvalidWithLogicExpire {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedissonClient redissonClient;


    @Test
    public void hotSpotInvalidWithLogicExpire() {
        // 模拟热点数据,注意这个时间是过期的
        String key = "name";

        // 查询热点数据,查询了一个过期的缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        RedisData bean = JSONUtil.toBean(json, RedisData.class);

        // 发现过期
        if (LocalDateTime.now().isAfter(bean.expireTime)) {
            // 拿锁重建建立缓存,覆盖过期缓存.如果没拿到锁就直接返回旧数据
            RLock lock = redissonClient.getLock("lock:" + key);
            if (lock.tryLock()) {
                // 要double check一下,是不是已经重构了缓存,这里重构缓存的识别点在这个过期时间
                bean = JSONUtil.toBean(stringRedisTemplate.opsForValue().get(key), RedisData.class);
                if (!LocalDateTime.now().isAfter(bean.expireTime)) {
                    System.out.println(Thread.currentThread()+"已经重构了数据,返回:" + bean.data);
                    return;
                }
                // 异步执行重构过程
                new Thread(() ->{
                    try {
                        RedisData newExpirationData = new RedisData(LocalDateTime.now().plusSeconds(20), "ljt1");
                        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(newExpirationData), 20, TimeUnit.SECONDS);
                        System.out.println(Thread.currentThread()+"newExpirationData = " + newExpirationData);
                    } finally {
                        // 应该是建立完缓存后释放锁
                        lock.unlockAsync();
                    }
                }).start();
                System.out.println(Thread.currentThread()+"开启线程重构了,返回旧数据:" + bean.data);
                return;
            } else {
                System.out.println(Thread.currentThread()+"返回旧数据:" + bean.data);
                return;
            }
        }
        // 没过期,直接返回
        System.out.println(Thread.currentThread()+"返回:" + bean.data);
        return;
    }

    private void initExpiredData(String key) {
        RedisData redisData = new RedisData(LocalDateTime.now().plusSeconds(-20), "ljt0");
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData), 20, TimeUnit.SECONDS);
    }

    @Test
    public void mockManyThreads() throws InterruptedException {
        initExpiredData("name");

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> hotSpotInvalidWithLogicExpire()).start();
            // 这个sleep等于1是正常的,但是这个sleep等于0就回出现问题,代码执行的特别快,缓存没有弄完就全部读完了,测试就没有了效果.
            Thread.sleep(1);
        }
        // 防止主线程先关闭,好像主线程关闭了,他这个redisConnection就会立即断开了,所以这里主线程有个sleep函数的调用.而且也是这里很妙
        // 如果你在外面调用Thread.sleep(1000*5),在for循环里面没有调用Thread.sleep(1),这可以试试为啥会没有演示的效果.
    }


    @Data
    @AllArgsConstructor
    public class RedisData implements Serializable {
        private LocalDateTime expireTime;
        private Object data;
    }

}


